﻿#region Copyright & licence

// This file is part of Refix.
// 
// Refix is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
// 
// Refix is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public
// License along with Refix.  If not, see <http://www.gnu.org/licenses/>.
// 
// Copyright (C) 2010-11 David Musgrove and others.

#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Web;
using System.Xml.Serialization;

using ICSharpCode.SharpZipLib.Checksums;
using ICSharpCode.SharpZipLib.Zip;

using Refix.Core.Properties;

namespace Refix.Core.Repository
{
    public class LocalRepository : ILocalRepository
    {
        private readonly string _baseDirectory;
        private readonly RemoteRepository _remoteRepository;

        public bool Offline { get; set; }

        public LocalRepository(string baseDirectory, RemoteRepository remoteRepository)
        {
            _baseDirectory = baseDirectory ?? ".refixrepo";
            if (!Path.IsPathRooted(BaseDirectory))
            {
                _baseDirectory = Path.Combine(Directory.GetCurrentDirectory(), BaseDirectory);
            }

            _remoteRepository = remoteRepository ?? new RemoteRepository(Configuration.RemoteRepository, BaseDirectory);
        }

        public LocalRepository() : this(Configuration.LocalRepository, null) { }
        public LocalRepository(string baseDirectory) : this(baseDirectory, null) { }

        public string BaseDirectory
        {
            get { return _baseDirectory; }
        }

        public bool Contains(string assemblyName)
        {
            return Contains(assemblyName, true);
        }

        private bool Contains(string assemblyName, bool download)
        {
            if (download && !Offline
                && !File.Exists(GetVersionFilePath(assemblyName)))
            {
                string successMessage = string.Format(
                    ResourceDispenser.GetString("LOCALREPOSITORY_DOWNLOADED_ASSEMBLY"),
                    assemblyName, _remoteRepository.Url);
                _remoteRepository.DownloadAllVersions(assemblyName, successMessage);
            }
            return File.Exists(GetVersionFilePath(assemblyName));
        }

        private string GetVersionFilePath(string assemblyName)
        {
            return Path.Combine(PathUtilities.GetComponentPath(BaseDirectory, assemblyName), "versions.xml");
        }

        public string GetAssemblyVersionFolder(Assembly assembly)
        {
            string assemblyName = assembly.GetName().Name;
            if (string.Equals(Path.GetExtension(assembly.Location), ".exe", StringComparison.OrdinalIgnoreCase))
            {
                assemblyName += ".exe";
            }
            return Path.Combine(PathUtilities.GetComponentPath(BaseDirectory, assemblyName), assembly.GetName().Version.ToString());
        }

        private string GetAssemblyFilePath(Assembly assembly, string clrVersion)
        {
            string folder = GetAssemblyVersionFolder(assembly);
            if (!clrVersion.IsNullOrEmpty())
            {
                folder = Path.Combine(folder, clrVersion);
            }
            return Path.Combine(folder, Path.GetFileName(assembly.Location));
        }

        public Component GetComponent(string assemblyName)
        {
            return GetComponent(assemblyName, true);
        }

        public Component GetComponent(string assemblyName, bool download)
        {
            if (!Contains(assemblyName, download))
            {
                return null;
            }
            string versionFilePath = GetVersionFilePath(assemblyName);
            using (FileStream fileStream = File.OpenRead(versionFilePath))
            {
                return (Component)new XmlSerializer(typeof(Component)).Deserialize(fileStream);
            }
        }

        public byte[] GetComponentVersionZipFile(string assemblyName, string version)
        {
            string componentPath = PathUtilities.GetComponentPath(BaseDirectory, assemblyName);
            if (!Directory.Exists(componentPath))
            {
                if (GetComponent(assemblyName, true) == null)
                {
                    return null;
                }
            }
            if (!string.IsNullOrEmpty(version)
                && !Directory.Exists(Path.Combine(componentPath, version)))
            {
                if (GetComponentVersion(assemblyName, version, true) == null)
                {
                    return null;
                }
            }
            var crc = new Crc32();
            byte[] buffer;
            int fileCount = 0;
            using (var ms = new MemoryStream())
            {
                using (var zipStream = new ZipOutputStream(ms))
                {
                    if (string.IsNullOrEmpty(version))
                    {
                        string versionFileName = Path.Combine(componentPath, "versions.xml");
                        if (File.Exists(versionFileName))
                        {
                            fileCount += AddFile(zipStream, null, versionFileName, crc);
                        }
                        fileCount += new DirectoryInfo(componentPath)
                            .GetDirectories()
                            .Sum(d => AddDirectory(zipStream, versionFileName, d.FullName, crc));
                    }
                    else
                    {
                        string versionFileName = Path.Combine(componentPath, "_versions.xml");
                        var component = GetComponent(assemblyName);
                        component.Versions.RemoveAll(v => v.Number != version);
                        using (FileStream fileStream = File.Open(versionFileName, FileMode.Create))
                        {
                            new XmlSerializer(typeof(Component)).Serialize(fileStream, component);
                        }
                        fileCount += AddFile(zipStream, null, versionFileName, crc);
                        fileCount += AddDirectory(zipStream, versionFileName, Path.Combine(componentPath, version), crc);
                        File.Delete(versionFileName);
                    }
                    zipStream.Finish();
                    if (fileCount == 0)
                    {
                        return null;
                    }
                    buffer = ms.ToArray();
                    zipStream.Close();
                }
            }
            return buffer;
        }

        static private int AddDirectory(ZipOutputStream zipStream, string versionFileName, string directoryName,
                                Crc32 crc)
        {
            int fileCount = new DirectoryInfo(directoryName)
                .GetFiles()
                .Sum(f => AddFile(zipStream, versionFileName, f.FullName, crc));
            fileCount += new DirectoryInfo(directoryName)
                .GetDirectories()
                .Sum(d => AddDirectory(zipStream, versionFileName, d.FullName, crc));
            return fileCount;
        }

        static private int AddFile(ZipOutputStream zipStream, string componentPath, string fileName, Crc32 crc)
        {
            if (!File.Exists(fileName))
            {
                return 0;
            }
            string relativeFileName = componentPath == null
                                          ? "_versions.xml"
                                          : PathUtilities.MakeRelativePath(componentPath, fileName);
            var entry = new ZipEntry(relativeFileName);
            byte[] buffer = File.ReadAllBytes(fileName);
            crc.Reset();
            crc.Update(buffer);
            entry.Size = buffer.Length;
            zipStream.PutNextEntry(entry);
            zipStream.Write(buffer, 0, buffer.Length);
            return 1;
        }

        public ComponentVersion GetComponentVersion(string assemblyName, string version)
        {
            return GetComponentVersion(assemblyName, version, true);
        }

        public ComponentVersion GetComponentVersion(string assemblyName, string version, bool download)
        {
            if (!Contains(assemblyName, download))
            {
                return null;
            }
            Component component = GetComponent(assemblyName, download);
            if (download && !Offline
                && !component.Versions.Any(v => v.Number == version)
                && !string.IsNullOrEmpty(_remoteRepository.Url))
            {
                string successMessage = string.Format(ResourceDispenser.GetString("LOCALREPOSITORY_DOWNLOADED_VERSION"),
                                                      version, assemblyName, _remoteRepository.Url);
                if (_remoteRepository.DownloadVersion(assemblyName, version, successMessage))
                {
                    component = GetComponent(assemblyName, false);
                }
            }
            return component.Versions.SingleOrDefault(v => v.Number == version);
        }

        public bool SynchronizeWithRemoteRepository(string assemblyName)
        {
            Component component = GetComponent(assemblyName, false);
            if (component == null)
            {
                OutputWriter.WriteLine(OutputVerbosity.Quiet, ResourceDispenser.GetString("UPLOAD_UNKNOWN_ASSEMBLY"),
                                   assemblyName);
                return false;
            }
            byte[] zipFile = GetComponentVersionZipFile(assemblyName, null);
            string urlBase = _remoteRepository.Url;
            if (urlBase.EndsWith("/")) urlBase = urlBase.Substring(0, urlBase.Length - 1);
            WebRequest webRequest = WebRequest.Create(string.Format("{0}/Synchronize.aspx/{1}/{2}",
                urlBase, assemblyName, HttpUtility.UrlEncode(Configuration.UploadEmail)));
            webRequest.Method = "put";
            webRequest.ContentType = "application/octet-stream";
            webRequest.ContentLength = zipFile.Length;
            try
            {
                Stream requestStream = webRequest.GetRequestStream();
                requestStream.Write(zipFile, 0, zipFile.Length);
                requestStream.Flush();
                requestStream.Close();
                webRequest.GetResponse();
            }
            catch (WebException ex)
            {
                var response = ex.Response as HttpWebResponse;
                if (response != null && response.StatusCode == HttpStatusCode.Unauthorized)
                {
                    OutputWriter.WriteLine(OutputVerbosity.Quiet, ResourceDispenser.GetString("UPLOAD_NOT_ALLOWED"),
                        _remoteRepository.Url);
                }
                else
                {
                    OutputWriter.WriteLine(OutputVerbosity.Quiet, ResourceDispenser.GetString("WEB_SERVER_NOT_REACHABLE"),
                        _remoteRepository.Url);
                }
                return false;
            }
            OutputWriter.WriteLine(ResourceDispenser.GetString("SYNCHRONIZE_UPLOADED"), assemblyName,
                _remoteRepository.Url);
            return true;
        }

        public bool UploadToRemoteRepository(string assemblyName, string version)
        {
            Component component = GetComponent(assemblyName, false);
            if (component == null)
            {
                OutputWriter.WriteLine(OutputVerbosity.Quiet, ResourceDispenser.GetString("UPLOAD_UNKNOWN_ASSEMBLY"),
                                   assemblyName);
                return false;
            }
            ComponentVersion componentVersion = GetComponentVersion(assemblyName, version);
            if (componentVersion == null)
            {
                OutputWriter.WriteLine(OutputVerbosity.Quiet, ResourceDispenser.GetString("UPLOAD_UNKNOWN_VERSION"),
                                   version, assemblyName);
                return false;
            }
            byte[] zipFile = GetComponentVersionZipFile(assemblyName, version);
            string urlBase = _remoteRepository.Url;
            if (urlBase.EndsWith("/")) urlBase = urlBase.Substring(0, urlBase.Length - 1);
            WebRequest webRequest = WebRequest.Create(string.Format("{0}/AcceptUpload.aspx/{1}/{2}/{3}",
                urlBase, assemblyName, version, HttpUtility.UrlEncode(Configuration.UploadEmail)));
            webRequest.Method = "put";
            webRequest.ContentType = "application/octet-stream";
            webRequest.ContentLength = zipFile.Length;
            try
            {
                Stream requestStream = webRequest.GetRequestStream();
                requestStream.Write(zipFile, 0, zipFile.Length);
                requestStream.Flush();
                requestStream.Close();
                webRequest.GetResponse();
            }
            catch (WebException ex)
            {
                var response = ex.Response as HttpWebResponse;
                if (response != null && response.StatusCode == HttpStatusCode.Unauthorized)
                {
                    OutputWriter.WriteLine(OutputVerbosity.Quiet, ResourceDispenser.GetString("UPLOAD_NOT_ALLOWED"),
                        _remoteRepository.Url);
                }
                else if (response != null && response.StatusCode == HttpStatusCode.Conflict)
                {
                    OutputWriter.WriteLine(OutputVerbosity.Quiet, ResourceDispenser.GetString("UPLOAD_ALREADY_EXISTS"),
                        version, assemblyName, _remoteRepository.Url);
                    return true;
                }
                else
                {
                    OutputWriter.WriteLine(OutputVerbosity.Quiet, ResourceDispenser.GetString("WEB_SERVER_NOT_REACHABLE"),
                        _remoteRepository.Url);
                }
                return false;
            }
            OutputWriter.WriteLine(ResourceDispenser.GetString("UPLOAD_VERSION_UPLOADED"), version, assemblyName,
                _remoteRepository.Url);
            return true;
        }

        public int SaveFiles(IEnumerable<Reference> assemblyBindingRedirects, string assemblyName, string version,
            string clrVersion, string folder)
        {
            string componentPath = Path.GetDirectoryName(GetVersionFilePath(assemblyName));
            if (version.IsNullOrEmpty()) return 0;
            string versionPath = Path.Combine(componentPath, version);
            if (!Directory.Exists(folder))
            {
                Directory.CreateDirectory(folder);
            }
            if (!Directory.Exists(versionPath) && GetComponentVersion(assemblyName, version) == null)
            {
                OutputWriter.WriteLine(OutputVerbosity.Quiet,
                                       ResourceDispenser.GetString("LOCAL_REPOSITORY_WARNING_NOT_FOUND"), assemblyName,
                                       version);
                return 0;
            }
            int filesCopied = CopyFilesRecursively(folder, new DirectoryInfo(versionPath), clrVersion);
            if (filesCopied == 0)
            {
                return 0;
            }
            ComponentVersion componentVersion = GetComponentVersion(assemblyName, version);
            foreach (Dependency dependency in componentVersion.Dependencies)
            {
                string dependencyVersion = dependency.Version;
                Dependency dependencyCopy = dependency;
                string dependencyVersionCopy = dependencyVersion;
                Reference redirect =
                    assemblyBindingRedirects.FirstOrDefault(
                        a => a.AssemblyName == dependencyCopy.Name && a.SpecifiedVersion == dependencyVersionCopy);
                if (redirect != null)
                {
                    dependencyVersion = redirect.ResolvedVersion;
                }
                filesCopied += SaveFiles(assemblyBindingRedirects, dependency.Name, dependencyVersion, clrVersion, folder);
            }
            return filesCopied;
        }

        private static int CopyFilesRecursively(string folder, DirectoryInfo versionPath, string clrVersion) {
            int filesCopied = 0;
            // copy files
            string sourceFolder = versionPath.FullName;
            if (!clrVersion.IsNullOrEmpty())
            {
                if (Directory.Exists(Path.Combine(sourceFolder, clrVersion)))
                {
                    sourceFolder = Path.Combine(sourceFolder, clrVersion);
                }
            }
            foreach (FileInfo fileInfo in new DirectoryInfo(sourceFolder).GetFiles())
            {
                string destination = Path.Combine(folder, fileInfo.Name);
                
                if (!File.Exists(destination))
                {
                    filesCopied++;
                    fileInfo.CopyTo(destination, true);
                }
            }

            // recurse subdirectories
            foreach (DirectoryInfo dirInfo in versionPath.GetDirectories().Where(f => !f.Name.StartsWith("net-")))
            {
                string subFolder = Path.Combine(folder, dirInfo.Name);
                var subFolderInfo = new DirectoryInfo(subFolder);
                subFolderInfo.Create();
                filesCopied += CopyFilesRecursively(subFolder, dirInfo, clrVersion);
            }

            return filesCopied;
        }

        public void PutComponent(Component component, bool incrementLocalRevision)
        {
            string versionFilePath = GetVersionFilePath(component.Name);
            string directoryName = Path.GetDirectoryName(versionFilePath);
            if (component.Versions.Count > 0)
            {
                EnsureDirectory(directoryName);
                if (incrementLocalRevision)
                {
                    if (component.LocalRevision == 0)
                    {
                        component.LocalRevision = component.RemoteRevision;
                    }
                    component.LocalRevision++;
                }
                using (FileStream fileStream = File.Open(versionFilePath, FileMode.Create))
                {
                    new XmlSerializer(typeof(Component)).Serialize(fileStream, component);
                }
            }
            else
            {
                if (File.Exists(versionFilePath))
                {
                    File.Delete(versionFilePath);
                }
            }
        }

        public void BackupVersionFile(Component component)
        {
            string versionFilePath = GetVersionFilePath(component.Name);
            string backupFilePath = string.Format("{0}.r{1}l{2}",
                                                  versionFilePath, component.RemoteRevision, component.LocalRevision);
            if (File.Exists(backupFilePath))
            {
                File.Delete(backupFilePath);
            }
            File.Copy(versionFilePath, backupFilePath);
        }

        public void PutComponent(Component component)
        {
            PutComponent(component, true);
        }

        public void RemoveAssembly(Component component, string version)
        {
            component.Versions.RemoveAll(v => v.Number == version);
            PutComponent(component);
            string versionFilePath = GetVersionFilePath(component.Name);
            string directoryName = Path.Combine(Path.GetDirectoryName(versionFilePath), version);
            if (Directory.Exists(directoryName))
            {
                Directory.Delete(directoryName, true);
            }
        }

        public void PutExternalReferences(Assembly assembly, string clrVersion, IEnumerable<string> externals)
        {
            string fileName = GetAssemblyFilePath(assembly, clrVersion);
            string directoryName = Path.GetDirectoryName(fileName);
            foreach (string external in externals)
            {
                string sourceFileName = Path.Combine(Path.GetDirectoryName(assembly.Location), external);
                if (File.Exists(sourceFileName))
                {
                    File.Copy(sourceFileName,
                              Path.Combine(directoryName, external), true);
                }
            }
        }

        public bool RegisterAssembly(Assembly assembly, string clrVersion)
        {
            AssemblyName assemblyName = assembly.GetName();
            string name = assemblyName.Name;
            if (string.Equals(Path.GetExtension(assembly.Location), ".exe", StringComparison.OrdinalIgnoreCase))
            {
                name += ".exe";
            }
            Component component = GetComponent(name) ?? new Component { Name = name };

            string versionNumber = assemblyName.Version.ToString();
            bool existingVersion = false;
            if (GetComponentVersion(name, versionNumber) != null)
            {
                existingVersion = true;
                if (!clrVersion.IsNullOrEmpty() && File.Exists(GetAssemblyFilePath(assembly, clrVersion)))
                {
                    using (OutputWriter.Highlight(ConsoleColor.Yellow))
                    {
                        OutputWriter.WriteLine(
                            ResourceDispenser.GetString("LOCAL_REPOSITORY_WARNING_VERSION_ALREADY_AVAILABLE"), name,
                            versionNumber);
                    }
                    return false;
                }
            }

            if (!existingVersion)
            {
                ComponentVersion version = AddNewVersion(component, assemblyName);
                AddDependencies(version, assembly);
                PutComponent(component);
            }
            PutAssembly(assembly, clrVersion);
            return true;
        }

        static private void AddDependencies(ComponentVersion version, Assembly assembly)
        {
            foreach (AssemblyName reference in assembly.GetReferencedAssemblies()
                .Where(a => !a.Name.IsSystemAssemblyName()))
            {
                var dependency = new Dependency { Name = reference.Name, Version = reference.Version.ToString() };
                version.Dependencies.Add(dependency);
            }
        }

        static private ComponentVersion AddNewVersion(Component component, AssemblyName assemblyName)
        {
            var version = new ComponentVersion
            {
                Number = assemblyName.Version.ToString(),
                Culture = assemblyName.CultureInfo.Name,
                PublicKey = assemblyName.GetPublicKeyToken().ToPublicKeyTokenString()
            };
            component.Versions.Add(version);
            return version;
        }

        public void PutAssembly(Assembly assembly, string clrVersion)
        {
            var otherFilesToCopy = new List<string>();
            string assemblyFileName = Path.GetFileName(assembly.Location);

            otherFilesToCopy.Add(Path.ChangeExtension(assemblyFileName, "xml"));
            otherFilesToCopy.Add(Path.ChangeExtension(assemblyFileName, "pdb"));
            otherFilesToCopy.Add(assemblyFileName + ".config");

            // Add satellite assemblies to the list
            string satelliteAssemblyName = string.Format(
                "{0}.resources{1}",
                Path.GetFileNameWithoutExtension(assemblyFileName),
                Path.GetExtension(assemblyFileName));

            var sourceDirectory = new DirectoryInfo(Path.GetDirectoryName(assembly.Location));

            otherFilesToCopy.AddRange(from di in sourceDirectory.GetDirectories()
                                      let s = Path.Combine(di.FullName, satelliteAssemblyName)
                                      where File.Exists(s)
                                      select Path.Combine(di.Name, satelliteAssemblyName));

            string destinationFileName = GetAssemblyFilePath(assembly, clrVersion);
            string destinationDirectoryName = Path.GetDirectoryName(destinationFileName);
            EnsureDirectory(destinationDirectoryName);

            // Copy the files in the assembly
            foreach (FileStream fileStream in assembly.GetFiles(true))
            {
                File.Copy(fileStream.Name, Path.Combine(destinationDirectoryName, Path.GetFileName(fileStream.Name)), true);
            }

            // And copy all 'other' related files.
            foreach (string otherFileName in otherFilesToCopy)
            {
                string otherSourceFileName = Path.Combine(sourceDirectory.FullName, otherFileName);
                string otherDestinationFileName = Path.Combine(destinationDirectoryName, otherFileName);
                if (File.Exists(otherSourceFileName))
                {
                    EnsureDirectory(Path.GetDirectoryName(otherDestinationFileName));
                    File.Copy(otherSourceFileName, otherDestinationFileName, true);
                }
            }
        }

        static private void EnsureDirectory(string directoryName)
        {
            if (!Directory.Exists(directoryName))
            {
                Directory.CreateDirectory(directoryName);
            }
        }

        public int GetRemoteComponentRevision(string assemblyName)
        {
            return _remoteRepository.GetComponentRevision(assemblyName);
        }

        public int GetComponentLocalRevision(string assemblyName)
        {
            var component = GetComponent(assemblyName, false);
            if (component == null)
            {
                return -1;
            }
            return component.LocalRevision;
        }

        public int GetComponentRemoteRevision(string assemblyName)
        {
            var component = GetComponent(assemblyName, false);
            if (component == null)
            {
                return -1;
            }
            return component.RemoteRevision;
        }
    }
}